在前一篇文章當中我們展示了如何使用 Rust 當中的 Rocket 以及 mongoDB 套件,建立了一個簡單的使用者資料管理方面的 RESTful API,但是總不能叫使用者只能透過 API 直接與資料庫溝通吧,這樣的話就不叫作 Web 應用了,當然還需要有前端作為使用者的操作介面囉,這時候我們就可以與之前提到的前端框架 React 進行結合,並且只需要簡單的幾個步驟即可完成這樣的全端開發。
為了要沿用上一篇的專案內容的 RESTful API 作為資料庫互動的後端,我們現在需要在既有的檔案結構下新增 React 專案,假設上一篇的檔案結構最後長這樣, rust-restful-api
專案是被放在 myproject
內:
myproject/
└── rust-restful-api/
├── src/
│ └── main.rs
├── target/
├── .gitignore
├── Cargo.lock
└── Cargo.toml
那我們現在可以在 myproject
路徑下在終端機輸入指令
npx create-react-app frontend
這樣就會建立一個名為 frontend
的前端專用資料夾,接著我們直接進入該資料夾,執行打包的指令
cd frontend
npm run build
完成之後檔案結構會長這樣
myproject/
├── frontend/
│ ├── build/
│ ├── node_modules/
│ ├── public/
│ ├── src/
│ ├── .gitignore
│ ├── package-lock.json
│ ├── package.json
│ └── README.md
└── rust-restful-api/
├── src/
└── target/
└──.gitignore
└──Cargo.lock
└──Cargo.toml
接下來,我們要先處理 Rust 這邊讀取前端網頁的程式碼
首先,修改一下 Rust 的 main.rs
,我們直接看修改後的完整程式碼,基於上一篇的範例修改的
use futures::stream::TryStreamExt; // 導入 TryStreamExt,提供 `try_collect` 方法以簡化處理 MongoDB 查詢結果
use mongodb::{bson::doc, bson::oid::ObjectId, options::ClientOptions, Client}; // 導入 MongoDB 客戶端、選項及 BSON 文檔的處理工具
use rocket::{delete, fs::FileServer, get, post, put, routes, serde::json::Json, State}; // 導入 Rocket 框架的路由、HTTP 請求方法及狀態管理
use serde::{Deserialize, Serialize}; // 導入 Serde 序列化及反序列化,讓結構體能夠轉換成 JSON
// 定義一個用戶結構,支援 JSON 的序列化和反序列化,並實現 Debug 和 Clone 特徵
#[derive(Debug, Deserialize, Serialize, Clone)]
struct User {
#[serde(skip_serializing_if = "Option::is_none")] // 如果 id 欄位是 None,序列化時會跳過該欄位
id: Option<String>, // 用戶的 ID,可能不存在(可選)
name: String, // 用戶的名字,為必填欄位
}
// Rocket 主程式入口,使用 async 函數以支援異步操作
#[rocket::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 設置 MongoDB 連接選項,使用 MongoDB Atlas 提供的 URL 進行連接
let client_options = ClientOptions::parse("mongodb+srv://masoufo0310:2ioxaagu@database.541vj.mongodb.net/?retryWrites=true&w=majority&appName=Database").await?;
let client = Client::with_options(client_options)?; // 建立 MongoDB 客戶端連接
let _db = client.database("testdb"); // 取得指定的資料庫 testdb
// 構建 Rocket 應用,並管理 MongoDB 客戶端狀態
rocket::build()
.manage(client) // 傳遞 MongoDB 客戶端到 Rocket 管理狀態
.mount(
// 註冊路由,將所有 CRUD 操作綁定到應用上
"/",
routes![create_user, get_users, update_user, delete_user], // 綁定路由,處理用戶的新增、查詢、更新、刪除
)
.mount("/", FileServer::from("../frontend/build")) // 讓 Rocket 提供 React build 目錄中的靜態資源
.ignite() // 啟動 Rocket 應用,初始化伺服器
.await?
.launch() // 啟動伺服器,並開始監聽請求
.await?;
Ok(()) // 正常退出時返回 OK
}
// POST 請求,用來新增用戶
#[post("/users", data = "<user>")]
async fn create_user(client: &State<Client>, user: Json<User>) -> Json<User> {
let collection = client.database("testdb").collection::<User>("users"); // 連接到 MongoDB 的 testdb 資料庫,並選擇 users 集合
let new_user = User {
id: None, // 新增用戶時,ID 設為 None,由 MongoDB 自動生成
name: user.name.clone(), // 複製請求中的用戶名稱
};
collection.insert_one(new_user.clone(), None).await.unwrap(); // 將新用戶資料插入資料庫,忽略錯誤處理
Json(new_user) // 回傳新增的用戶資料作為 JSON
}
// GET 請求,用來獲取所有用戶資料
#[get("/users")]
async fn get_users(client: &State<Client>) -> Json<Vec<User>> {
let collection = client.database("testdb").collection::<User>("users"); // 連接到 MongoDB 的 testdb 資料庫,並選擇 users 集合
let cursor = collection.find(None, None).await.unwrap(); // 從集合中查詢所有用戶,獲取 MongoDB 游標
let users: Vec<User> = cursor.try_collect().await.unwrap(); // 將游標中的用戶資料轉換為 Vec<User>
Json(users) // 回傳所有用戶的 JSON 數據
}
// PUT 請求,用來更新指定 ID 的用戶資料
#[put("/users/<id>", data = "<user>")]
async fn update_user(client: &State<Client>, id: String, user: Json<User>) -> Json<User> {
let collection = client.database("testdb").collection::<User>("users");
// 將 id 轉換為 ObjectId
let object_id = match ObjectId::parse_str(&id) {
Ok(oid) => oid,
Err(_) => {
return Json(User {
id: Some(id),
name: "Invalid ID format".to_string(),
})
}
};
let filter = doc! { "_id": object_id }; // 根據 ObjectId 進行過濾
let update = doc! { "$set": { "name": &user.name } }; // 設置更新條件
collection.update_one(filter, update, None).await.unwrap();
Json(User {
id: Some(id), // 更新後的用戶 ID 設為指定的 ID
name: user.name.clone(), // 返回更新後的用戶名字
})
}
// DELETE 請求,用來刪除指定 ID 的用戶
#[delete("/users/<id>")]
async fn delete_user(client: &State<Client>, id: String) -> String {
let collection = client.database("testdb").collection::<User>("users"); // 連接到 MongoDB 的 testdb 資料庫,並選擇 users 集合
// 將傳入的 id 轉換為 ObjectId,如果失敗則回傳錯誤訊息
let object_id = match ObjectId::parse_str(&id) {
Ok(oid) => oid,
Err(_) => return format!("Invalid ID format: {}", id),
};
let filter = doc! { "_id": object_id }; // 使用 BSON 格式,根據 ObjectId 進行過濾
// 執行刪除操作,忽略錯誤處理
match collection.delete_one(filter, None).await {
Ok(result) => {
if result.deleted_count == 1 {
format!("User with id {} deleted", id) // 回傳刪除成功訊息
} else {
format!("No user found with id {}", id) // 如果沒有找到用戶,回傳未找到訊息
}
}
Err(_) => format!("Failed to delete user with id {}", id), // 刪除失敗時回傳錯誤訊息
}
}
這段程式碼 98% 都跟上一篇的一模一樣,只有少少的新增了兩行:
use rocket::{delete, fs::FileServer, get, post, put, routes, serde::json::Json, State};
,在這當中加入了 fs::FileServer
,它的作用是讓 Rocket 提供靜態資源。這意味著,我們可以指向一個特定的目錄,Rocket 會將該目錄中的檔案(如 HTML、JavaScript、CSS 等)直接提供給用戶端。在這個案例中,我們將 React 前端應用打包後的靜態檔案放在 ../frontend/build
目錄中,而 FileServer 就是負責將這些靜態檔案提供給用戶。.mount("/", FileServer::from("../frontend/build"))
,在 rocket::build
內加入了當使用者進入 /
路徑時,以 ../frontend/build
作為 FileServer
他就是預設將該路徑資料夾中的 index.html 作為回傳的檔案內容,並且也能讓用戶讀取得到裡面的 static
靜態資源。實際執行的時間到了
cargo run
執行後可以看到以下畫面出現在 http://127.0.0.1:8000
就代表我們已經透過 rocket 完整的結合了 React 跟 Rust 的前後端,接下來讓我們完成前端用戶介面吧
現在我們可以再開一個終端機,以區別原本的後端所使用的終端機,並且先進入 React資料夾下
cd frontend
我們會需要透過前端去呼叫 API ,所以這邊先用指令安裝一下 React 常用的 request 套件 axios
npm install axios
完成套件安裝之後,我們需要修改兩個檔案,首先第一個檔案是位於 src
之下的 App.js
,他原本的內容應該是長這樣的
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
可能有人對於 React 還並不是很認識,但我們就先略過關於這段程式碼當中的介紹,先以概念與流程為主,簡單來說,這個 App.js
的內容就是我們剛剛上面網頁中所呈現的內容主體,因此我們只需要修改這個 App.js
就可以重新建立我們的首頁畫面,接下來我們就直接進行修改,修改後的 App.js
內容如下:
import { useState, useEffect } from 'react'; // 從 React 中導入 useState 和 useEffect,用來管理狀態與副作用
import axios from 'axios'; // 從 axios 套件導入,用來處理 HTTP 請求
import './App.css'; // 導入外部的 CSS 檔案,設置樣式
function App() {
const [users, setUsers] = useState([]); // 定義一個狀態變數 users,用來儲存用戶列表,初始值為空陣列
const [newUserName, setNewUserName] = useState(''); // 定義一個狀態變數 newUserName,用來儲存新增用戶的名字,初始值為空字串
const [updateUserName, setUpdateUserName] = useState(''); // 定義一個狀態變數 updateUserName,用來儲存更新用戶的名字,初始值為空字串
const [userIdToUpdate, setUserIdToUpdate] = useState(''); // 定義一個狀態變數 userIdToUpdate,用來儲存需要更新的用戶 ID,初始值為空字串
// 在組件首次渲染後,執行獲取用戶列表的函數
useEffect(() => {
fetchUsers(); // 執行獲取用戶的函數
}, []); // 這裡的空陣列表示只在組件首次載入時執行一次
// 獲取用戶列表的非同步函數
const fetchUsers = async () => {
try {
const response = await axios.get('/users'); // 向後端發送 GET 請求,獲取用戶資料
setUsers(response.data); // 將獲取到的用戶資料設定到 users 狀態中
} catch (error) {
console.error('Error fetching users:', error); // 如果發生錯誤,顯示錯誤訊息
}
};
// 新增用戶的非同步函數
const createUser = async () => {
try {
if (!newUserName) return; // 如果沒有輸入用戶名稱,則不執行新增操作
await axios.post('/users', { name: newUserName }); // 向後端發送 POST 請求,傳送新用戶資料
setNewUserName(''); // 清空輸入框的內容
fetchUsers(); // 重新獲取用戶列表,更新畫面
} catch (error) {
console.error('Error creating user:', error); // 如果發生錯誤,顯示錯誤訊息
}
};
// 刪除用戶的非同步函數
const deleteUser = async (id) => {
try {
await axios.delete(`/users/${id}`); // 向後端發送 DELETE 請求,刪除指定 ID 的用戶
fetchUsers(); // 重新獲取用戶列表,更新畫面
} catch (error) {
console.error('Error deleting user:', error); // 如果發生錯誤,顯示錯誤訊息
}
};
// 更新用戶的非同步函數
const updateUser = async () => {
try {
if (!userIdToUpdate || !updateUserName) return; // 如果沒有輸入要更新的用戶 ID 或新名稱,則不執行更新操作
await axios.put(`/users/${userIdToUpdate}`, { name: updateUserName }); // 向後端發送 PUT 請求,更新指定 ID 的用戶資料
setUserIdToUpdate(''); // 清空輸入框的內容
setUpdateUserName(''); // 清空輸入框的內容
fetchUsers(); // 重新獲取用戶列表,更新畫面
} catch (error) {
console.error('Error updating user:', error); // 如果發生錯誤,顯示錯誤訊息
}
};
return (
<div className="App"> {/* 最外層的 div,套用 CSS 樣式 App */}
<header className="App-header"> {/* 頁面的 header,套用 CSS 樣式 App-header */}
<h1>會員資料管理系統</h1> {/* 頁面標題 */}
<div className="form-section"> {/* 用於表單的區塊,包含新增和更新會員 */}
<div className="form-group"> {/* 表單組塊,用於新增會員 */}
<h2>新增會員</h2> {/* 小標題:新增會員 */}
<input
type="text"
value={newUserName} // 綁定新用戶名稱的狀態變數
onChange={(e) => setNewUserName(e.target.value)} // 當輸入變更時,更新 newUserName 狀態
placeholder="輸入會員名稱" // 提示使用者輸入會員名稱
/>
<button onClick={createUser}>新增會員</button> {/* 點擊按鈕時執行 createUser 函數 */}
</div>
<div className="form-group"> {/* 表單組塊,用於更新會員 */}
<h2>更新會員</h2> {/* 小標題:更新會員 */}
<input
type="text"
value={userIdToUpdate} // 綁定需要更新的會員 ID 的狀態變數
onChange={(e) => setUserIdToUpdate(e.target.value)} // 當輸入變更時,更新 userIdToUpdate 狀態
placeholder="輸入會員ID" // 提示使用者輸入會員 ID
/>
<input
type="text"
value={updateUserName} // 綁定新的會員名稱的狀態變數
onChange={(e) => setUpdateUserName(e.target.value)} // 當輸入變更時,更新 updateUserName 狀態
placeholder="輸入新的會員名稱" // 提示使用者輸入新的會員名稱
/>
<button onClick={updateUser}>更新會員</button> {/* 點擊按鈕時執行 updateUser 函數 */}
</div>
</div>
<div className="table-section"> {/* 表格區塊,用於顯示會員列表 */}
<h2>會員列表</h2> {/* 小標題:會員列表 */}
<table className="user-table"> {/* 使用者表格,套用 CSS 樣式 user-table */}
<thead>
<tr>
<th>會員ID</th> {/* 表格標題:會員 ID */}
<th>會員名稱</th> {/* 表格標題:會員名稱 */}
<th>操作</th> {/* 表格標題:操作 */}
</tr>
</thead>
<tbody>
{users.map((user) => ( // 遍歷 users 陣列,顯示每個用戶的資料
<tr key={user.id}> {/* 每一列的唯一 key 為 user.id */}
<td>{user.id}</td> {/* 顯示用戶的 ID */}
<td>{user.name}</td> {/* 顯示用戶的名稱 */}
<td>
<button className="edit-btn" onClick={() => deleteUser(user.id)}>刪除</button> {/* 刪除按鈕,點擊後執行 deleteUser 函數 */}
</td>
</tr>
))}
</tbody>
</table>
</div>
</header>
</div>
);
}
export default App; // 導出 App 組件,以便在其他地方使用
修改完畢之後,我們要針對網頁的樣式進一步修改,所以我們也同步修改這個 App.css
的內容如下:
.App {
text-align: center;
background-color: #f9f3e7;
color: #4f4f4f;
font-family: 'Arial', sans-serif;
}
.App-header {
background-color: #f3e5d5;
padding: 20px;
border-radius: 10px;
}
h1 {
color: #c48d41;
}
.form-section {
display: flex;
justify-content: space-around;
margin: 20px 0;
}
.form-group {
background-color: #fff4e6;
padding: 15px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
input {
margin: 10px;
padding: 10px;
border: 2px solid #c48d41;
border-radius: 5px;
}
button {
padding: 10px 20px;
background-color: #e07a5f;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #c5533d;
}
.table-section {
margin-top: 20px;
}
.user-table {
width: 80%;
margin: 0 auto;
border-collapse: collapse;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.user-table th,
.user-table td {
padding: 10px 15px;
border: 1px solid #ddd;
}
.user-table th {
background-color: #f7d6ad;
color: #4f4f4f;
}
.user-table td {
background-color: #fffaf0;
}
.edit-btn {
background-color: #e07a5f;
color: white;
padding: 5px 10px;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.edit-btn:hover {
background-color: #c5533d;
}
完成之後,我們再次打包,每次前端修改完畢之後,都要重新打包,這樣才能刷新後端那邊讀取到的 build
資料夾內的檔案
npm run build
打包完畢之後,我們再次查看前端網頁現在的樣子,後端這邊也需要重新載入,只需要按下 Ctrl + F5 刷新即可
現在我們就可以透過前端介面跟資料庫溝通囉,如果我們要修改網頁內容,就只需要更改 App.js
或者 App.css
之後,用 npm run build
重新打包就可以了,這樣我們就可以同時使用 React 與 Rust 的高效能進行開發了
上面展示了我們將兩者之間結合的結果,現在我們要開始解析 App.js
的網頁內容了
在這個段落,我們將深入探討 App.js
檔案中的核心概念,這些概念能夠幫助你更好地理解如何透過 React 與後端 API 進行互動,並創建一個有效的前端資料管理介面。
useState
)首先,我們利用了 React 的 useState
來管理應用的狀態。在 App.js
中,我們使用了四個狀態變數來處理不同的需求:
users
: 用來存放從後端取得的用戶列表,這是一個陣列,初始值為空陣列。newUserName
: 儲存用戶在新增會員時輸入的名稱。updateUserName
: 儲存用戶在更新會員時輸入的新的名稱。userIdToUpdate
: 儲存用戶在更新會員時輸入的用戶 ID。這些狀態變數可以透過 setUsers
、setNewUserName
、setUpdateUserName
和 setUserIdToUpdate
來動態更新。這些變數在 React 組件的生命週期內,會根據使用者的操作而改變,並導致畫面重新渲染。
useEffect
來處理資料的初始化載入useEffect
是 React 中用來處理副作用(side effects)的 Hook,在這個應用中,我們使用 useEffect
來在組件載入時從後端 API 獲取資料。具體來說,我們在 useEffect
中調用了 fetchUsers
函數,這個函數會在組件第一次被載入時自動執行,向後端發送 GET 請求來獲取所有的會員資料。
以下是 useEffect
的簡單工作流程:
useEffect(() => {
fetchUsers(); // 組件載入後獲取用戶資料
}, []); // 空陣列表示該副作用只在組件首次載入時執行一次
React 前端與後端 API 的互動是透過 axios
這個 HTTP 客戶端庫來實現的。axios
允許我們使用簡單的函數來發送 GET、POST、PUT、DELETE 等 HTTP 請求,並處理回應。在這個應用中,分別有三個主要的操作:
獲取用戶列表 (GET 請求):
我們使用 axios.get
來向 /users
路徑發送 GET 請求,並將後端回應的用戶資料存入 users
狀態變數中,這樣就能夠在頁面上顯示用戶列表。
const fetchUsers = async () => {
try {
const response = await axios.get('/users'); // 向後端發送 GET 請求
setUsers(response.data); // 將獲取到的用戶列表設定到狀態中
} catch (error) {
console.error('Error fetching users:', error); // 處理錯誤
}
};
新增用戶 (POST 請求):
當使用者在表單中輸入會員名稱並點擊 "新增會員" 按鈕後,createUser
函數會發送 POST 請求到 /users
,新增一個新的用戶。
const createUser = async () => {
try {
if (!newUserName) return; // 如果沒有輸入用戶名稱,則不執行
await axios.post('/users', { name: newUserName }); // 發送 POST 請求新增用戶
setNewUserName(''); // 清空輸入框
fetchUsers(); // 重新獲取用戶列表
} catch (error) {
console.error('Error creating user:', error); // 處理錯誤
}
};
更新用戶 (PUT 請求):
當使用者輸入用戶 ID 和新的名稱後,點擊 "更新會員" 按鈕,updateUser
函數會發送 PUT 請求來更新對應的用戶資料。
const updateUser = async () => {
try {
if (!userIdToUpdate || !updateUserName) return; // 檢查是否輸入了用戶 ID 和新名稱
await axios.put(`/users/${userIdToUpdate}`, { name: updateUserName }); // 發送 PUT 請求更新用戶資料
setUserIdToUpdate(''); // 清空輸入框
setUpdateUserName(''); // 清空輸入框
fetchUsers(); // 重新獲取用戶列表
} catch (error) {
console.error('Error updating user:', error); // 處理錯誤
}
};
刪除用戶 (DELETE 請求):
當使用者點擊 "刪除" 按鈕後,deleteUser
函數會發送 DELETE 請求,從後端刪除對應的用戶資料。
const deleteUser = async (id) => {
try {
await axios.delete(`/users/${id}`); // 發送 DELETE 請求刪除用戶
fetchUsers(); // 重新獲取用戶列表
} catch (error) {
console.error('Error deleting user:', error); // 處理錯誤
}
};
我們的前端頁面有兩個主要的表單區域,一個用來新增會員,另一個用來更新會員。使用者輸入名稱或 ID 時,這些值會即時存入對應的狀態變數,並在點擊按鈕時觸發相應的函數。
newUserName
,按鈕則綁定 createUser
函數。userIdToUpdate
和 updateUserName
,按鈕則綁定 updateUser
函數。每次新增、更新或刪除會員後,React 組件會自動重新渲染,並顯示最新的用戶列表,這是因為我們在每次操作後都會呼叫 fetchUsers
函數來更新 users
狀態。
在表格區域中,我們使用了 map
函數來遍歷 users
陣列,並生成每個用戶的表格行。對於每個用戶,我們顯示其 ID、名稱,並附上 "刪除" 按鈕。當點擊刪除按鈕時,會呼叫 deleteUser
函數,根據用戶的 ID 將其從資料庫中刪除。
這個 App.js
檔案展示了 React 前端如何透過 API 與後端服務進行互動。React 的 useState
和 useEffect
Hook 幫助我們簡化了狀態管理和資料載入的邏輯,而 axios
庫則讓我們能夠輕鬆發送 HTTP 請求。在此基礎上,這個用戶管理系統可以進一步擴展,加入更多功能如驗證、分頁、排序等。
這篇文章結合了使用Rust + React作為全端開發的基礎,回顧一開始從 Python 開發者的角度切入,到目前為止已經經歷了27天,對於 Rust 雖然不敢說熟悉,但是也因此增加了很多基礎知識與概念,現在再加上 React 的前端開發進來,可以說是在網頁開發上的一大進展,雖然 Python 的 Django 與 flask 等框架也是提供了非常簡易的網頁開發能力,相較之下 Rust 的 API 撰寫上當然也是比較困難的,但為了使用到 Rust 的高效能,讓它能夠擔任後端處理的工作也是相當不錯的選擇。
React 作為目前最熱門的前端框架之一,自然也是有許多可以適用的套件能供開發者選擇,由於今天的文章當中僅以 App.js
單一檔案為範例,再接下來,我們需要補充最後一個Web應用開發必要的部分,來解釋如果我的網頁需要有多個分頁,則應該怎麼去修改,畢竟總不能只使用一個分頁就完成整個網站的開發吧
也趁這個機會,下一篇我們將會再次說明 React 網頁開發的方式是怎麼一回事,希望也能提供給對 React 不熟悉的人有更進一步的認識囉